/**
 * Copyright (c) 2012 Nokia Corporation. 
 */

package com.nokia.example.tictactoe.connectivity;

import com.nokia.example.tictactoe.console.Log;

/**
 * The connection manager.
 */
public class ConnectionManager
	extends Connection
	implements Connection.IConnectionManager
{
	// Constants
	public static final Integer SERVER_PORT = new Integer(13001);
	public static final int DISCOVERY_TIMEOUT = 30000; // Milliseconds
	
	// Connection IDs
	private static final int DISCOVERY_CLIENT = 0;
	private static final int DISCOVERY_SERVER = 1;
	private static final int TCP_CLIENT = 2;
	private static final int TCP_SERVER = 3;
	private static final int CONNECTION_COUNT = 4;
	
	// Members
	private static ConnectionManager _instance = null; // Singleton instance
	private Connection[] _connections = null;
	private boolean _isServer = false;
	
	/** 
	 * @param listener The listener for the manager. Can be null.
	 * @return The connection manager instance.
	 */
	static public ConnectionManager instance(Connection.Listener listener) {
		if (_instance == null) {
			_instance = new ConnectionManager(listener);
		}
		else {
			_instance.addListener(listener);
		}
		
		return _instance;
	}
	
	/**
	 * Constructor.
	 * @param listener The listener for the manager. Can be null.
	 */
	private ConnectionManager(Connection.Listener listener) {
		_state = NOT_CONNECTED;
		_connections = new Connection[CONNECTION_COUNT];
		addListener(listener);
	}

	/**
	 * From Connection. Overriden.
	 */
	public boolean addListener(Connection.Listener listener) {
		if (_connections[0] == null) {
			// No connections created yet. Create them now
			createConnections();
		}
		
		for (int i = 0; i < CONNECTION_COUNT; ++i) {
			if (i != DISCOVERY_SERVER) {
				_connections[i].addListener(listener);
			}
		}
		
		return super.addListener(listener);
	}
	
	/**
	 * From Connection. Overriden.
	 */
	public boolean removeListener(Connection.Listener listener) {
		for (int i = 0; i < CONNECTION_COUNT; ++i) {
			if (_connections[i] != null && i != DISCOVERY_SERVER) {
				_connections[i].removeListener(listener);
			}
		}
		
		return super.removeListener(listener);
	}
	
	/**
	 * From Connection.
	 */
	public boolean open(String hostname, Integer port) {
		return connect();
	}

	/**
	 * From Connection.
	 */
	public boolean send(byte[] message) {
		if (_state != CONNECTED) {
			return false;
		}
		
		if (_isServer && _connections[TCP_SERVER] != null) {
			return _connections[TCP_SERVER].send(message);
		}
		else if (!_isServer && _connections[TCP_CLIENT] != null) {
			return _connections[TCP_CLIENT].send(message);
		}
		
		return false;
	}
	
	/**
	 * From Connection.
	 */
	public void close() {
		disconnect();
	}
	
	// Methods from Connection.IConnectionManager ->
	
	public void onConnected(Connection connection) {
		if (connection != _connections[TCP_CLIENT]
				&& connection != _connections[TCP_SERVER])
		{
			System.out.println("ConnectionManager::onConnected(): UDP based connections do not count.");
			return;
		}
		
		for (int i = 0; i < CONNECTION_COUNT; ++i) {
			if (_connections[i] != connection) {
				_connections[i].close();
			}
		}
		
		if (connection == _connections[TCP_CLIENT]) {
			_isServer = false;
		}
		else {
			_isServer = true;
		}
		
		setState(CONNECTED);
	}
	
	public void onDisconnected(Connection connection) {
		if (pendingConnections() == 0) {
			setState(NOT_CONNECTED);
		}
	}
	
	public void onTimeout() {
		notifyTimeout();
		disconnect();
	}
	
	public void onFatalError(Connection connection, String errorMessage) {
		if (_state == CONNECTED
				&& (connection == _connections[TCP_CLIENT]
					|| connection == _connections[TCP_SERVER]))
		{
			// Let's just assume it was the peer who disconnected
			notifyPeerDisconnected();
		}
		else {
			notifyError("Failed to connect: " + errorMessage);
		}
		
		disconnect();
	}

	public void onLogRequest(String message) {
		System.out.println("LOG: " + message);
		Log.addEntry(message);
	}
	
	// <- methods from Connection.IConnectionManager
	
	/**
	 * Starts discovery, and connects after a peer has been found.
	 * @return True if started discovering successfully, false otherwise.
	 */
	public boolean connect() {
		System.out.println("ConnectionManager::connect()");
		
		if (pendingConnections() > 0) {
			// Previous connections pending so we need to disconnect first
			System.out.println("ConnectionManager::connect(): Cannot connect due to pending connections.");
			disconnect();
			return false;
		}
		
		setState(DISCOVERING);
		onLogRequest("Starting discovery...");
		
		createConnections();
		
		if (!_connections[TCP_SERVER].open(null, SERVER_PORT)) {
			notifyError("TCP server failed to start!");
			disconnect();
			return false;
		}
		
		if (!((DiscoveryServer)(_connections[DISCOVERY_SERVER])).startListening()) {
			notifyError("Discovery server failed to start!");
			disconnect();
			return false;
		}
		
		_connections[DISCOVERY_CLIENT].open(null, null);
		return true;
	}
	
	/**
	 * Disconnects/stops discovery.
	 */
	public void disconnect() {
		System.out.println("ConnectionManager::disconnect()");
		
		if (_state != NOT_CONNECTED) {
			// Would be crazy to jump from "Not connected" to "Disconnecting"
			setState(DISCONNECTING);
		}
		
		for (int i = 0; i < CONNECTION_COUNT; ++i) {
			if (_connections[i] != null
					&& (_connections[i].state() == CONNECTING
						|| _connections[i].state() == CONNECTED))
			{
				_connections[i].close();
			}
		}
		
		// The state change is made in onDisconnected() once every connection
		// has been successfully disconnected.
	}
	
	/** 
	 * @return True if connected as a server, false otherwise.
	 */
	public boolean isServer() {
		return _isServer;
	}
	
	/**
	 * Deletes all clients and servers and sets the members to null.
	 */
	public void forceDisconnect() {
		if (_state != NOT_CONNECTED) {
			setState(DISCONNECTING);
		}
		
		for (int i = 0; i < CONNECTION_COUNT; ++i) {
			_connections[i].clearAllListeners();
			_connections[i].close();
			_connections[i] = null;
		}
		
		setState(NOT_CONNECTED);
	}
	
	/**
	 * Creates the connections.
	 */
	private void createConnections() {
		if (_connections[TCP_CLIENT] == null) {
			_connections[TCP_CLIENT] = new TcpClient();
			_connections[TCP_CLIENT].setConnectionManager(this);
		}
		
		if (_connections[TCP_SERVER] == null) {
			_connections[TCP_SERVER] = new TcpServer();
			_connections[TCP_SERVER].setConnectionManager(this);
		}
		
		if (_connections[DISCOVERY_CLIENT] == null) {
			_connections[DISCOVERY_CLIENT] = new DiscoveryClient();
			_connections[DISCOVERY_CLIENT].setConnectionManager(this);
			((DiscoveryClient)(_connections[DISCOVERY_CLIENT])).setTimeout(DISCOVERY_TIMEOUT);
		}
		
		if (_connections[DISCOVERY_SERVER] == null) {
			_connections[DISCOVERY_SERVER] =
					new DiscoveryServer((TcpClient)_connections[TCP_CLIENT]);
			_connections[DISCOVERY_SERVER].setConnectionManager(this);
		}
	}
	
	/** 
	 * @return The number of connections that are not in
	 * 		   Connection.NOT_CONNECTED state.
	 */
	private final int pendingConnections() {
		int numOfConnections = 0;
		
		for (int i = 0; i < CONNECTION_COUNT; ++i) {
			if (_connections[i] != null
					&& _connections[i].state() != NOT_CONNECTED)
			{
				System.out.println("ConnectionManager::pendingConnections(): Pending ID: " + i);
				numOfConnections++;
			}
		}
		
		return numOfConnections;
	}
}
